fix: deduplicate config listeners by identity#121
fix: deduplicate config listeners by identity#121nobodyiam wants to merge 1 commit intoapolloconfig:mainfrom
Conversation
📝 WalkthroughWalkthroughReplaces equality-based listener deduplication in AbstractConfig with identity-based checks so distinct listener instances with identical characteristics can be registered, removed, and notified independently; adds unit tests validating multi-instance notification and removal; updates CHANGES.md with a changelog entry. Changes
Sequence Diagram(s)sequenceDiagram
participant Initializer as ApolloApplicationContextInitializer
participant Config as AbstractConfig
participant SourceA as CachedCompositePropertySource#A
participant SourceB as CachedCompositePropertySource#B
Note over Initializer,Config: Bootstrap and application contexts create distinct CachedCompositePropertySource instances with same name
Initializer->>Config: addChangeListener(listenerA, keysA, prefixesA)
Config-->>Config: containsListenerInstance? (identity) → false
Config-->>Config: add listenerA (store interested keys/prefixes)
Initializer->>Config: addChangeListener(listenerB, keysB, prefixesB)
Config-->>Config: containsListenerInstance? (identity) → false
Config-->>Config: add listenerB
Config->>SourceA: notify changeEvent
SourceA-->>SourceA: onChange -> clear cached names
SourceA-->>Config: handler invoked (listenerA or listenerB depending on instance)
Config->>SourceB: notify changeEvent
SourceB-->>SourceB: onChange -> clear cached names
SourceB-->>Config: handler invoked (distinct listener instances both get events)
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~20 minutes Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing touches
🧪 Generate unit tests (beta)
No actionable comments were generated in the recent review. 🎉 🧹 Recent nitpick comments
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
apollo-client/src/main/java/com/ctrip/framework/apollo/internals/AbstractConfig.java (1)
113-123:⚠️ Potential issue | 🟠 Major
m_interestedKeys/m_interestedKeyPrefixesremoval still usesequals, inconsistent with identity-basedm_listeners.When two listeners are equal-but-not-identical (the exact scenario this PR fixes),
m_interestedKeys.remove(listener)on line 115 usesequals-based lookup on theConcurrentHashMap, so removing either listener deletes the shared map entry for both. The surviving listener then has no entry inm_interestedKeys, causingisConfigChangeListenerInterestedto treat it as interested in all keys — a silent broadening of its subscription scope.The same issue applies to
addChangeListener:m_interestedKeys.put(listener2, ...)overwrites the entry keyed bylistener1(since ConcurrentHashMap usesequals), so listener1's interested-key set is silently replaced by listener2's.Consider switching these maps to an
IdentityHashMap(wrapped withCollections.synchronizedMapfor thread safety) to match the identity semantics used form_listeners.Proposed fix sketch
- private final Map<ConfigChangeListener, Set<String>> m_interestedKeys = Maps.newConcurrentMap(); - private final Map<ConfigChangeListener, Set<String>> m_interestedKeyPrefixes = Maps.newConcurrentMap(); + private final Map<ConfigChangeListener, Set<String>> m_interestedKeys = + Collections.synchronizedMap(new IdentityHashMap<>()); + private final Map<ConfigChangeListener, Set<String>> m_interestedKeyPrefixes = + Collections.synchronizedMap(new IdentityHashMap<>());And in
removeChangeListener, move the map removals inside the identity check so only the matched instance's entries are cleaned up:public boolean removeChangeListener(ConfigChangeListener listener) { - m_interestedKeys.remove(listener); - m_interestedKeyPrefixes.remove(listener); for (ConfigChangeListener addedListener : m_listeners) { if (addedListener == listener) { + m_interestedKeys.remove(addedListener); + m_interestedKeyPrefixes.remove(addedListener); return m_listeners.remove(addedListener); } } return false; }
🧹 Nitpick comments (1)
apollo-client/src/test/java/com/ctrip/framework/apollo/internals/AbstractConfigTest.java (1)
128-156: Good regression test that clearly validates the fix.The precondition assertions on lines 140-141 are excellent — they explicitly verify the conditions (
assertNotSame+assertEquals) that trigger the bug.Minor robustness note:
Thread.sleep(100)on line 152 is inherently racy. The existing tests in this class useSettableFuturewith a timeout for deterministic waiting. Consider adopting the same pattern here for consistency and CI stability.
427c8d8 to
6d32daa
Compare
There was a problem hiding this comment.
Pull request overview
This PR addresses a Spring Cloud bootstrap dual-context edge case where change listeners that are different instances but equals()-equal (e.g., Spring PropertySource equality-by-name) could be incorrectly de-duplicated, leading to missed notifications and stale cached property names.
Changes:
- Switch
AbstractConfigchange-listener de-duplication fromequals()to instance identity (==). - Update listener removal logic to attempt identity-based removal.
- Add a regression test covering two same-name
CachedCompositePropertySourcelisteners and updateCHANGES.md.
Reviewed changes
Copilot reviewed 3 out of 3 changed files in this pull request and generated 2 comments.
| File | Description |
|---|---|
apollo-client/src/main/java/com/ctrip/framework/apollo/internals/AbstractConfig.java |
Changes listener registration/removal logic to use identity semantics. |
apollo-client/src/test/java/com/ctrip/framework/apollo/internals/AbstractConfigTest.java |
Adds regression test for two equals()-equal but distinct listener instances both receiving events. |
CHANGES.md |
Documents the fix in release notes. |
Comments suppressed due to low confidence (1)
apollo-client/src/main/java/com/ctrip/framework/apollo/internals/AbstractConfig.java:109
m_interestedKeys/m_interestedKeyPrefixesare still keyed byConfigChangeListenerin aConcurrentMap, so they still useequals/hashCode. For listeners whereequalsis not identity (e.g., SpringPropertySourceequality-by-name), registering two distinct instances will collide and overwrite each other’s interested-keys/prefixes, and later lookups/removals won’t be identity-safe. To truly switch to identity semantics, store per-listener metadata with identity keys (e.g., wrap the listener in an identity-based key, or replace the separate maps with a single registration object that holds listener + filters).
public void addChangeListener(ConfigChangeListener listener, Set<String> interestedKeys, Set<String> interestedKeyPrefixes) {
if (!containsListenerInstance(listener)) {
m_listeners.add(listener);
if (interestedKeys != null && !interestedKeys.isEmpty()) {
m_interestedKeys.put(listener, Sets.newHashSet(interestedKeys));
}
if (interestedKeyPrefixes != null && !interestedKeyPrefixes.isEmpty()) {
m_interestedKeyPrefixes.put(listener, Sets.newHashSet(interestedKeyPrefixes));
}
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
apollo-client/src/main/java/com/ctrip/framework/apollo/internals/AbstractConfig.java
Outdated
Show resolved
Hide resolved
apollo-client/src/test/java/com/ctrip/framework/apollo/internals/AbstractConfigTest.java
Show resolved
Hide resolved
6d32daa to
ad4aa96
Compare
ad4aa96 to
85b3cf4
Compare
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 3 out of 3 changed files in this pull request and generated no new comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
What's the purpose of this PR
Fixes a listener registration edge case in Spring Cloud bootstrap dual-context initialization.
When two
CachedCompositePropertySourcelisteners share the same name (ApolloBootstrapPropertySources),AbstractConfig#addChangeListenerusedList.contains(equals-based) to de-duplicate listeners.Because Spring
PropertySourcedefines equality by name, the second listener instance could be skipped,which leaves the active context listener unsubscribed and
CachedCompositePropertySource#namesstale after key add/delete.This PR switches listener de-duplication/removal to instance identity (
==) semantics and adds a regression testcovering two same-name
CachedCompositePropertySourcelisteners.Which issue(s) this PR fixes:
Fixes #88
Brief changelog
AbstractConfig#addChangeListenerto deduplicate by object identity instead ofequalsAbstractConfig#removeChangeListenerto remove by object identityCachedCompositePropertySourcelisteners both receiving change eventsCHANGES.mdFollow this checklist to help us incorporate your contribution quickly and easily:
mvn clean testto make sure this pull request doesn't break anything.CHANGESlog.Test run details
mvn -pl apollo-client clean testmvn -pl apollo-client -Dtest=AbstractConfigTest,CachedCompositePropertySourceTest,ApolloApplicationContextInitializerTest testSummary by CodeRabbit
Bug Fixes
Tests